package com.zz.common.service

import java.time.Instant
import java.util.UUID

import cats.data.OptionT
import cats.{Applicative, Functor, Monad, Traverse}
import cats.implicits._
import com.typesafe.scalalogging.Logger
import com.zz.ady.idl.IdType
import com.zz.common.zid.ZidStorage
import com.zz.idl.{IdBundle, IdBundleMeta, ReversedIndexMeta}

package object IdService {
  type Zid = String

  trait ZidserviceTrait[F[_]] {

    protected[this] def zidStorage: ZidStorage[F]

    def storeOrUpdate(idBundle: IdBundle)(implicit monad: Monad[F]): OptionT[F, Zid] = {
      if (idBundle.mid.nonEmpty) {//id is existent, find idBundle by mid
        tryMergeAndStore(idBundle, idBundle.mid, IdType.MID)
      }else if (idBundle.imei.nonEmpty) {//imei is existent, find idBundle by imei
        tryMergeAndStore(idBundle, idBundle.imei, IdType.IMEI)
      }else if (idBundle.imeiMd5.nonEmpty){
        tryMergeAndStore(idBundle, idBundle.imeiMd5, IdType.IMEI_MD5)
      }else if (idBundle.imeiSha1.nonEmpty){
        tryMergeAndStore(idBundle, idBundle.imeiSha1, IdType.IMEI_SHA1)
      }else {
        OptionT.liftF(
          for{
            uuid <- newUUID()
            idBundleWithZid = idBundle.withZid(uuid)
            now = Instant.now.toEpochMilli
            idBundleMeta = IdBundleMeta(
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now,
              now)
            _ <- zidStorage.storeIdBundle(idBundleWithZid, idBundleMeta)
            _ <- storeReversedIndex(idBundleWithZid)
          }yield uuid
        )
      }
    }

    def storeOrUpdateInBatch[A[_]](idBundles: A[IdBundle])
                                  (implicit traverse: Traverse[A]
                                   , monad:Monad[F]): OptionT[F, A[Zid]] =
      traverse.traverse(idBundles)(idBundle => storeOrUpdate(idBundle))

    def queryZid(id: String, idType: IdType)(implicit monad:Monad[F]): OptionT[F, Zid] = {
      queryMostConfidentIdBundle(id, idType).map(_._1.zid)
    }

    protected[this] def pickMostConfidentZid(bundlesAndMeta: Seq[(IdBundle, IdBundleMeta)]): Option[(IdBundle, IdBundleMeta)] = {
      //select latest zid
      bundlesAndMeta.sortBy { case (_, idBundleMeta) => idBundleMeta.zidUpdatedAt }
        .lastOption
    }

    protected[this] def mergeIdBundle(existent: IdBundle, idBundleMeta: IdBundleMeta, newOne: IdBundle): (IdBundle, IdBundleMeta) = {
      var idBundle = existent
      var idBundleMeta_ = idBundleMeta
      if (newOne.zid.nonEmpty){//if zid is existent, then newOne is a totally new IdBundle
        idBundle = idBundle.withZid(newOne.zid)
        idBundleMeta_ = idBundleMeta_.withZidUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.imei != newOne.imei) {
        idBundle = idBundle.withImei(newOne.imei)
        idBundleMeta_ = idBundleMeta_.withImeiUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.imeiMd5 != newOne.imeiMd5) {
        idBundle = idBundle.withImeiMd5(newOne.imeiMd5)
        idBundleMeta_ = idBundleMeta_.withImeiMd5UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.imeiSha1 != newOne.imeiSha1) {
        idBundle = idBundle.withImeiSha1(newOne.imeiSha1)
        idBundleMeta_ = idBundleMeta_.withImeiSha1UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.imsi != newOne.imsi) {
        idBundle = idBundle.withImsi(newOne.imsi)
        idBundleMeta_ = idBundleMeta_.withImsiUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.imsiMd5 != newOne.imsiMd5) {
        idBundle = idBundle.withImsiMd5(newOne.imsiMd5)
        idBundleMeta_ = idBundleMeta_.withImsiMd5UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.imsiSha1 != newOne.imsiSha1) {
        idBundle = idBundle.withImsiSha1(newOne.imsiSha1)
        idBundleMeta_ = idBundleMeta_.withImsiSha1UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.androidId != newOne.androidId) {
        idBundle = idBundle.withAndroidId(newOne.androidId)
        idBundleMeta_ = idBundleMeta_.withAndroidIdUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.androidIdMd5 != newOne.androidIdMd5) {
        idBundle = idBundle.withAndroidIdMd5(newOne.androidId)
        idBundleMeta_ = idBundleMeta_.withAndroidIdMd5UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.androidIdSha1 != newOne.androidIdSha1) {
        idBundle = idBundle.withAndroidIdSha1(newOne.androidIdSha1)
        idBundleMeta_ = idBundleMeta_.withAndroidIdSha1UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.mac != newOne.mac) {
        idBundle = idBundle.withMac(newOne.mac)
        idBundleMeta_ = idBundleMeta_.withMacUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.macMd5 != newOne.macMd5) {
        idBundle = idBundle.withMac(newOne.macMd5)
        idBundleMeta_ = idBundleMeta_.withMacMd5UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.macSha1 != newOne.macSha1) {
        idBundle = idBundle.withMacSha1(newOne.macSha1)
        idBundleMeta_ = idBundleMeta_.withMacSha1UpdateAt(Instant.now.toEpochMilli)
      }
      if (existent.sn != newOne.sn) {
        idBundle = idBundle.withSn(newOne.sn)
        idBundleMeta_ = idBundleMeta_.withSnUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.idfa != newOne.idfa) {
        idBundle = idBundle.withIdfa(newOne.idfa)
        idBundleMeta_ = idBundleMeta_.withIdfaUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.idfv != newOne.idfv) {
        idBundle = idBundle.withIdfv(newOne.idfv)
        idBundleMeta_ = idBundleMeta_.withIdfvUpdatedAt(Instant.now.toEpochMilli)
      }
      if (existent.mid != newOne.mid) {
        idBundle = idBundle.withMid(newOne.mid)
        idBundleMeta_ = idBundleMeta_.withMidUpdatedAt(Instant.now.toEpochMilli)
      }
      (idBundle, idBundleMeta_)
    }

    protected[this] def newUUID(): F[String]

    private[this] def tryMergeAndStore(idBundle:IdBundle, keyID:String, idType:IdType)(implicit monad: Monad[F]):OptionT[F, Zid] =
      for {//TODO seems problematic
        idBundle_idMeta <- queryMostConfidentIdBundle(keyID, idType)
          //generate a totally new zid if no zid found
            .orElseF(newUUID().map(uuid=>Some((
          idBundle.withZid(uuid),
          IdBundleMeta(
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli,
            Instant.now.toEpochMilli)
          ))))
        newIdBundleAndIdMeta <- OptionT.liftF(monad.pure(
          mergeIdBundle(idBundle_idMeta._1, idBundle_idMeta._2, idBundle)
        ))
        _ <- OptionT.liftF(zidStorage.storeIdBundle(newIdBundleAndIdMeta._1, newIdBundleAndIdMeta._2))
        _ <- OptionT.liftF(storeReversedIndex(newIdBundleAndIdMeta._1))
      } yield idBundle_idMeta._1.zid

    private[this] def storeReversedIndex(idBundle: IdBundle)(implicit monad: Monad[F]): F[Unit] = {
      for {
        _ <- if(idBundle.imei.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imei, IdType.IMEI, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.imeiMd5.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imeiMd5, IdType.IMEI_MD5, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.imeiSha1.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imeiSha1, IdType.IMEI_SHA1, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.imsi.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imsi, IdType.IMSI, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.imsiMd5.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imsiMd5, IdType.IMSI_MD5, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.imsiSha1.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.imsiSha1, IdType.IMSI_SHA1, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.androidId.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.androidId, IdType.ANDROID_ID, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.androidIdMd5.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.androidIdMd5, IdType.ANDROID_ID_MD5, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.androidIdSha1.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.androidIdSha1, IdType.ANDROID_ID_SHA1, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.mac.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.mac, IdType.MAC, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.macMd5.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.macMd5, IdType.MAC_MD5, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.macSha1.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.macSha1, IdType.MAC_SHA1, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.sn.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.sn, IdType.SN, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.idfa.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.idfa, IdType.IDFA, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.idfv.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.idfv, IdType.IDFV, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
        _ <- if(idBundle.mid.nonEmpty){
          zidStorage.storeReversedIndex(idBundle.mid, IdType.MID, idBundle.zid, ReversedIndexMeta(Instant.now.toEpochMilli))
        }else monad.unit
      }yield ()
    }
    private[this] def queryMostConfidentIdBundle(id:String, idType: IdType)
                                                (implicit monad:Monad[F]):OptionT[F, (IdBundle, IdBundleMeta)] ={
      for{
        zid_zids_meta <- zidStorage.queryZids(id, idType)
        bundleAndMetas <- zid_zids_meta._2.toList.traverse(zidStorage.queryZidInfo(_))
        mostConfidentZidInfo <- OptionT(monad.pure(pickMostConfidentZid(bundleAndMetas)))
      }yield mostConfidentZidInfo
    }
  }


}
